Skip to content

Fix generic SFC findComponent types - #2934

Merged
cexbrayat merged 3 commits into
vuejs:mainfrom
simonyang08:codex/fix-generic-sfc-find-component
Sep 4, 2026
Merged

Fix generic SFC findComponent types#2934
cexbrayat merged 3 commits into
vuejs:mainfrom
simonyang08:codex/fix-generic-sfc-find-component

Conversation

@simonyang08

Copy link
Copy Markdown
Contributor

Fixes #2436

Summary

  • recognize generic SFC call signatures in findComponent
  • return VueWrapper instead of falling through to DOMWrapper<Node>
  • add declaration-level regression coverage for <script setup generic> components

Root cause

Generic SFCs emitted by Vue's <script setup generic> syntax expose a generic call signature rather than the construct signature used by DefinedComponent. The existing overloads therefore selected the DOM-wrapper fallback.

Compatibility

The new overload is limited to generic call signatures returning VNode. Existing functional-component and defined-component overloads remain unchanged. The result intentionally uses an unparameterized VueWrapper because findComponent cannot instantiate the SFC's generic type parameter soundly.

Validation

  • actual generic-SFC vue-tsc reproduction - failed before, passed after
  • pnpm tsd - passed
  • pnpm test --run tests/findComponent.spec.ts - 36 passed
  • pnpm build - passed
  • pnpm lint - passed with unrelated existing warnings
  • changed-file formatting and git diff --check - passed

AI disclosure

This change was prepared with OpenAI Codex assistance and reviewed and validated locally by the contributor.

@netlify

netlify Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploy Preview for vue-test-utils-docs ready!

Name Link
🔨 Latest commit 7bd2bef
🔍 Latest deploy log https://app.netlify.com/projects/vue-test-utils-docs/deploys/6a9a300211b1f500077171d1
😎 Deploy Preview https://deploy-preview-2934--vue-test-utils-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@simonyang08 simonyang08 changed the title [codex] Fix generic SFC findComponent types Fix generic SFC findComponent types Aug 28, 2026
@simonyang08
simonyang08 force-pushed the codex/fix-generic-sfc-find-component branch from 09567a6 to 93f1339 Compare September 2, 2026 03:21
@simonyang08
simonyang08 marked this pull request as ready for review September 2, 2026 03:21
@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vue/test-utils@2934

commit: 7bd2bef

Comment thread src/interfaces/wrapperLike.ts Outdated
): VueWrapper<InstanceType<T>>
// Generic SFCs emitted by vue-tsc have a generic call signature instead of
// the construct signature used by DefinedComponent.
findComponent<T extends <U>(...args: any[]) => VNode>(selector: T): VueWrapper

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the same be done for getComponent and findAllComponent?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also afraid that this matches any generic function returning VNode, including ordinary generic functional components. Those produce a DOMWrapperat runtime, so properties such as vm incorrectly type-check. The overload needs to identify vue-tsc’s generic-SFC shape

Address cexbrayat's review of PR vuejs#2934:

1. Tighten the generic SFC overload so it no longer matches ordinary generic
   functional components. The overload's return type uses a conditional on
   '"__ctx" extends keyof ReturnType<T>' to resolve to VueWrapper only when
   vue-tsc's __ctx discriminator is present in the selector's return type,
   with an 'unknown extends ReturnType<T>' guard ahead of it because Vue's
   FunctionalComponent call signature returns `any`, which would otherwise
   satisfy the discriminator and wrongly resolve to VueWrapper. Plain generic
   functional components and FunctionalComponent selectors fall through to
   DOMWrapper, so .vm no longer type-checks on them.

2. Apply the same discriminated overload to getComponent and findAllComponents
   so all three APIs handle vue-tsc generic SFCs consistently.

3. Extend test-dts/genericComponent.d-test.ts to cover all three APIs for
   vue-tsc-shaped generic SFCs (assert VueWrapper), plain generic functional
   components and FunctionalComponent-typed components (assert DOMWrapper +
   @ts-expect-error on .vm), plus WrapperLike-level assertions mirroring the
   BaseWrapper overloads.

Signed-off-by: simonyang08 <ppt5928@gmail.com>
@simonyang08

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review — both points are addressed in the latest push (fea3b56).

1. Same treatment for getComponent and findAllComponents.

Done — both now carry the same discriminated overload, with return types mirroring their existing overloads (Omit<VueWrapper, 'exists'> / VueWrapper[] on the vue-tsc path, and the DOM wrapper types they already return otherwise).

2. Discriminating vue-tsc generic SFCs from ordinary generic functional components.

Valid concern — the original constraint matched any VNode-returning function. I verified empirically (running vue-tsc --declaration on a minimal <script setup lang="ts" generic="..."> SFC) that vue-tsc emits VNode & { __ctx?: ... }, while ordinary generic functionals return bare VNode. The discrimination now happens in the return type: '__ctx' extends keyof ReturnType<T> ? VueWrapper : DOMWrapper<...>.

While implementing, one more edge turned up that needed an explicit guard: Vue's FunctionalComponent call signature returns any, which satisfies the VNode-returning constraint and makes '__ctx' extends keyof ReturnType<T> evaluate to true (keyof any includes every string). The overloads therefore start with an unknown extends ReturnType<T> check that routes FunctionalComponent selectors to the DOM wrapper before the __ctx check. With that in place, FunctionalComponent-typed components resolve to DOMWrapper on all three APIs and .vm no longer type-checks on them — asserted via @ts-expect-error in test-dts/genericComponent.d-test.ts, along with WrapperLike-level assertions mirroring the BaseWrapper overloads.

pnpm run tsd is green, the full vitest suite passes (503 tests), and oxlint/oxfmt are clean on the changed files. Ready for another look.

@cexbrayat cexbrayat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if it is a bit too much of LLM gibberish, it sounds like a correct approach.

Just update the test and I think we'll be good to merge.

Comment thread test-dts/genericComponent.d-test.ts Outdated
__VLS_ctx?: any,
__VLS_expose?: any,
__VLS_setup?: Promise<any>
) => VNode & { __ctx?: any }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to use a real <script setup generic="Item"> component in a .vue file imported by the test, instead of relying on internals of vue-tsc

Address cexbrayat's review of PR vuejs#2934:

Replace the hand-written VueTscGenericSfc declaration in
test-dts/genericComponent.d-test.ts with a real <script setup>
generic SFC (test-dts/GenericSfc.vue) so the assertions exercise the
actual shape vue-tsc emits, not a synthetic mock of its internals.

Switch the tsd script in package.json from plain tsc to vue-tsc so the
.vue import is resolved; vue-tsc was already a devDependency and was
the tool that produced the __ctx-on-return-type shape the overload in
src/baseWrapper.ts keys on.

The three sets of assertions (vue-tsc SFC -> VueWrapper + .vm,
plain generic functional component -> DOMWrapper + @ts-expect-error
on .vm, FunctionalComponent shape -> DOMWrapper + @ts-expect-error
on .vm, WrapperLike mirror) are preserved unchanged.

Verified:
- vue-tsc --noEmit -p test-dts/tsconfig.tsd.json: clean with src fix
- same command against dist rebuilt from pre-fix src: fails as
  expected (generic functional component wrongly resolves as
  VueWrapper; existing @ts-expect-error directives report 'unused')
- vitest run: 503 passed / 1 todo across 54 files (no runtime change)
- oxlint / oxfmt --check on changed files: clean

Signed-off-by: simonyang08 <ppt5928@gmail.com>
@simonyang08

Copy link
Copy Markdown
Contributor Author

Thanks — fair point. Dropped the hand-written declaration and added test-dts/GenericSfc.vue, a real <script setup generic="Item extends string | number"> SFC that the d-test imports, so the assertions now exercise the actual type vue-tsc emits. Since plain tsc can't resolve .vue, the tsd script now runs vue-tsc --noEmit instead (vue-tsc was already a devDependency, and it's the same tool that produces the __ctx shape the overload discriminates on).

The negative cases are unchanged: plain generic functionals still resolve to DOMWrapper with @ts-expect-error on .vm, and the FunctionalComponent shape is covered the same way.

New commit: 7bd2bef. vue-tsc, vitest (56 focused / 503 full) and lint are all green.

@cexbrayat
cexbrayat merged commit 133e80c into vuejs:main Sep 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: TypeScript errors with Vue generic components

3 participants